package uk.co.q3c.deplan.server.dao;

import java.util.ArrayList;
import java.util.Random;

import org.apache.log4j.Level;
import org.apache.log4j.Logger;

import uk.co.q3c.deplan.client.admin.AdminObject;
import uk.co.q3c.deplan.client.service.PlanManager;
import uk.co.q3c.deplan.server.admin.ServerServiceFactory;
import uk.co.q3c.deplan.server.domain.util.shard.DatastoreCounter;
import uk.co.q3c.deplan.server.domain.util.shard.DatastoreCounterShard;

import com.google.appengine.api.datastore.QueryResultIterable;
import com.google.appengine.api.datastore.QueryResultIterator;
import com.google.appengine.repackaged.com.google.common.collect.Lists;
import com.googlecode.objectify.Query;

public class AdminDao_BigT extends DomainDao_BigT implements AdminDao {
	transient protected final Logger logger = Logger.getLogger(getClass().getName());

	/**
	 * @param connection
	 */
	public AdminDao_BigT(ServerServiceFactory factory) {
		super(factory);
	}

	@Override
	public int countCounters() {

		Query<DatastoreCounter> query = ofy().query(DatastoreCounter.class);
		return executeCountAdmin(query);
	}

	@Override
	public long counterValue(String counterName) {
		long sum = 0;
		Query<DatastoreCounterShard> q = ofy().query(
				DatastoreCounterShard.class)
				.filter("counterName", counterName);
		QueryResultIterable<DatastoreCounterShard> results = execute(q);
		ArrayList<DatastoreCounterShard> shards = Lists.newArrayList(results);
		if (logger.isDebugEnabled()) {
			logger.debug("There are " + shards.size() + " shards for counter '"
					+ counterName + "'");
		}

		if (shards.size() == 0) {
			logger.error("There are no shards for counter " + counterName);
			return 0;
		}
		// sum of all shard counts
		if (shards != null && !shards.isEmpty()) {
			for (DatastoreCounterShard current : shards) {
				sum += current.getCount();
			}
		}
		if (logger.isDebugEnabled()) {
			logger.debug("Returning value of " + sum + " for counter '"
					+ counterName + "'");
		}

		return sum;
	}

	private <T extends AdminObject> QueryResultIterable<T> execute(
			Query<T> query) {
		return query.fetch();

	}

	@Override
	public long incrementCounter(String counterName) {
		DatastoreCounter storedCounter = storedCounter(counterName);
		if (storedCounter == null) {
			logger.error("Unable to load counter for " + counterName);
			return 0;
		}
		// Find how many shards are in this counter.
		int shardCount = storedCounter.getShardCount();
		// Choose the shard randomly from the available shards.
		Random generator = new Random();
		int shardNumber = generator.nextInt(shardCount);
		// build the query definition
		Query<DatastoreCounterShard> query = ofy().query(
				DatastoreCounterShard.class)
				.filter("counterName", counterName)
				.filter("shardNumber", shardNumber);
		if (logger.isDebugEnabled()) {
			logger.debug("Selecting stored counter shard with " + query);
		}
		QueryResultIterator<DatastoreCounterShard> results = execute(query).iterator();

		if (!results.hasNext()) {
			logger.error("Failed to find shard " + shardNumber
					+ " for counter.  Unable to increment counter for  "
					+ counterName);
			return 0;
		}
		DatastoreCounterShard shard = results.next();
		shard.increment(1);
		save(shard);
		return counterValue(counterName);
	}

	private DatastoreCounter storedCounter(String counterName) {
		Query<DatastoreCounter> q = ofy().query(DatastoreCounter.class)
				.filter("counterName", counterName);
		if (logger.isDebugEnabled()) {
			logger.debug("Selecting stored counter with " + q);
		}
		DatastoreCounter storedCounter = null;
		QueryResultIterator<DatastoreCounter> results = execute(q).iterator();
		if (results.hasNext()) {
			storedCounter = results.next();
		}
		if (storedCounter == null) {
			if (logger.isEnabledFor(Level.ERROR)) {
				logger.error("Stored counter not found with name "
						+ counterName);
			}
		}
		return storedCounter;
	}

	@Override
	public void save(AdminObject adminObject) {
		if (logger.isDebugEnabled()) {
			logger.debug("Saving " + adminObject.getClass()
					.getSimpleName());
		}
		ofy().put(adminObject);

	}

	@Override
	public int countShardsInCounter(String counterName) {
		throw new RuntimeException("not yet implemented");
	}

	@Override
	public int addCounterShards(String counterName, int newShards) {

		// how many shards are there now?

		if (logger.isDebugEnabled()) {
			logger.debug("Adding " + newShards + " new shards to counter "
					+ counterName);
		}

		DatastoreCounter storedCounter = storedCounter(counterName);
		if (storedCounter == null) {
			logger.error("Counter for '" + counterName
					+ "' was not found in the datastore");
			return 0;
		}
		Integer scount = storedCounter.getShardCount();
		int currentShards = 0;
		if (scount != null) {
			currentShards = scount.intValue();
		}

		// increment by new amount
		storedCounter.setShardCount(currentShards + newShards);
		save(storedCounter);
		// ofy.close();

		// create new shards as needed
		if (logger.isDebugEnabled()) {
			logger.debug("Number of shards before adding new ones is "
					+ currentShards);
		}
		for (int i = 0; i < newShards; i++) {
			DatastoreCounterShard newShard = new DatastoreCounterShard();
			newShard.setCounterName(counterName);
			newShard.setShardNumber(currentShards);
			if (logger.isDebugEnabled()) {
				logger.debug("saving shard number " + currentShards + " of "
						+ counterName);
			}
			save(newShard);
			currentShards++;
		}

		return currentShards;
	}

	protected int executeCountAdmin(Query<? extends AdminObject> query) {
		int c = query.countAll();
		if (logger.isDebugEnabled()) {
			logger.debug("Return count using " + query.toString()
					+ " result = " + c);
		}
		return c;
	}

	/**
	 * Sets up a counter with a single shard - normally the key would be from a
	 * counter, but this method is called to create the counters.
	 * 
	 * @param counterName
	 * @param keyId
	 */
	@Override
	public void createCounter(String counterName) {
		// set keyID manually - 0 is not allowed
		DatastoreCounter dsc = new DatastoreCounter();
		dsc.setCounterName(counterName);
		dsc.setShardCount(1);
		save(dsc);

		// can't use counter because counter not ready for the key yet - create
		// shards directly
		// addCounterShards(1);
		DatastoreCounterShard newShard = new DatastoreCounterShard();
		newShard.setCounterName(counterName);
		newShard.setShardNumber(0);
		// keys cannot have a 0 value, but keyId is incremented before use.
		newShard.setCount(0);
		save(newShard);

	}

	@Override
	public PlanManager loadPlanManager() {
		throw new RuntimeException("not yet implemented");
	}

}
